<?php
namespace App\Libraries\NoSql\Mongodb;

use MongoDB\Driver\Manager,
	MongoDB\Driver\WriteConcern,
	MongoDB\Driver\BulkWrite,
	MongoDB\Driver\Query,
	MongoDB\Driver\ReadPreference,
	\MongoDB\BSON\ObjectId;

/**
 * Mongodb 客户端 php_mongodb 扩展实现
 * @package App\Libraries\NoSql\Mongodb
 */
class MongodbClient {

	#region 属性

	/**
	 * Mongodb驱动管理对象
	 * @var Manager
	 */
	private $manager;

	/**
	 * 数据库名称
	 * @var string
	 */
	private $dbName;

	/**
	 * Mongdodb配置信息
	 * @var \Phalcon\Config
	 */
	private $config;

	/**
	 * 要返回的字段
	 * @var array
	 */
	private $fields = array();

	/**
	 * 设置要返回的字段
	 * @param array $fields 查询数据要返回的字段
	 * @param boolean $hasId 是否需要返回_id字段
	 * @return $this
	 */
	public function fields(array $fields, $hasId = TRUE) {
		if ($fields) {
			$this->fields = array();
			$this->fields["_id"] = $hasId ? 1 : 0;
			foreach ($fields as $field) {
				$this->fields[$field] = 1;
			}
		}
		return $this;
	}

	/**
	 * 查询数据的查询条件
	 * @var array
	 */
	private $where = array();

	/**
	 * 设置查询数据的查询条件
	 * @param array $where 查询数据的查询条件
	 * @return $this
	 */
	public function where(array $where) {
		if ($where && isset($where["id"])) {
			if (is_array($where["id"])) {
				if (isset($where["id"]['$in'])) {
					$arr = $where["id"]['$in'];
					$in = array();
					foreach ($arr as $id) {
						$in[] = new ObjectId($id);
					}
					$where["_id"] = array('$in' => $in);
				}
			} else {
				$where["_id"] = new ObjectId($where["id"]);
			}
			unset($where["id"]);
		}
		$this->where = array_merge($where, $this->where);
		return $this;
	}

	/**
	 * 排序字段 1位升序，-1降序
	 * @var array
	 */
	private $sort = array();

	/**
	 * 排序字段
	 * @param array $sort 排序字段  1位升序，-1降序
	 * @return $this
	 */
	public function sort(array $sort) {
		$this->sort = $sort;
		return $this;
	}

	/**
	 * 限制 设置返回结合中匹配搜索条件的前N个数据项
	 * @var array
	 */
	private $limit = 0;

	/**
	 * 设置限制数
	 * @param $limit 限制数
	 * @return $this
	 */
	public function limit($limit) {
		$this->limit = intval($limit);
		return $this;
	}

	/**
	 * 忽略数 忽略前n个匹配搜索条件的结构
	 * @var array
	 */
	private $skip = 0;

	/**
	 * 设置忽略数
	 * @param $skip 忽略数
	 * @return $this
	 */
	public function skip($skip) {
		$this->skip = intval($skip);
		return $this;
	}

	/**
	 * 指定查询数据时使用的索引
	 * @var array
	 */
	private $hint = array();

	/**
	 * 指定查询数据时使用的索引
	 * @param array $hint 查询数据时使用的索引
	 * @return $this
	 */
	public function hint(array $hint) {
		$this->hint = $hint;
		return $this;
	}

	/**
	 * 需要更新的数据
	 * @var array
	 */
	private $update_data = array();

	#endregion

	#region 构造函数

	/**
	 * 构造函数
	 * @param $config MongoDb 相关配置信息
	 * @param string $dbName 数据库名称
	 */
	public function __construct($config, $dbName = "") {
		$this->config = $config;
		$this->dbName = $config->options->db;
	}

	#endregion

	#region 插入数据

	/**
	 * 插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 要插入的数据
	 * @param array $options 插入数据的参数
	 * @return bool
	 */
	public function insert($collectionName, array $data, array $options = array("fsync" => false, "j" => 1, "w" => 1)) {
		$this->_init();
		$bulk = new BulkWrite;
		$bulk->insert($data);
		$writeConcern = $this->_getWriteConcern($options);
		$result = $this->manager->executeBulkWrite("{$this->dbName}.{$collectionName}", $bulk, $writeConcern);
		if ($result && $result->getInsertedCount()) {
			return true;
		}
		return false;
	}

	/**
	 * 批量插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 要批量插入的数据
	 * @param array $options 插入数据的参数
	 * @return $this
	 */
	public function batchInsert($collectionName, array $data, array $options = array("fsync" => false, "j" => 1, "w" => 1)) {
		$this->_init();
		$bulk = new BulkWrite;
		foreach ($data as $document) {
			$bulk->insert($document);
		}
		$writeConcern = $this->_getWriteConcern($options);
		$result = $this->manager->executeBulkWrite("{$this->dbName}.{$collectionName}", $bulk, $writeConcern);
		if ($result && $result->getInsertedCount()) {
			return true;
		}
		return false;
	}

	#endregion

	#region 更新数据

	/**
	 * 更新/插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 更新匹配记录的对象
	 * @param array $options 更新的相关参数
	 * @throws \Exception
	 */
	public function save($collectionName, array $data, array $options = array("multiple" => true, "fsync" => false, "w" => 1, "j" => false)) {
		// 不实现该方法
		throw new Exception("mongodb 扩展不实现该方法");
	}

	/**
	 * 更新数据
	 * @param $collectionName 集合名称
	 * @param array $data 更新匹配记录的对象
	 * @param array $options 更新的相关参数
	 * @return bool
	 * @throws \Exception
	 */
	public function update($collectionName, array $data = array(), array $options = array("multiple" => true, "upsert" => true, "fsync" => false, "w" => 1, "j" => false)) {
		if (empty($data) && empty($this->update_data)) {
			throw new \Exception("没有设置要更新的对象");;
		}
		if (!empty($data)) {
			$data = array_merge($this->update_data, $data);
		} else {
			$data = $this->update_data;
		}
		$this->update_data = array();

		$where = $this->where;
		$this->where = array();

		$this->_init();
		$bulk = new BulkWrite;
		$bulk->update($where, $data, array('multi' => $options["multiple"], 'upsert' => $options["upsert"]));

		$writeConcern = $this->_getWriteConcern($options);
		$result = $this->manager->executeBulkWrite("{$this->dbName}.{$collectionName}", $bulk, $writeConcern);
		if ($result->getModifiedCount()) {
			return true;
		}
		return false;
	}

	/**
	 * 以原子的方式修改文档
	 * @param $collectionName 集合名称
	 * @param array|NULL $data 用于更新文档的信息
	 * @param array $options 指定选项
	 * @throws \Exception
	 */
	public function findAndModify($collectionName, array $data = NULL,
								  array $options = array("sort" => null, "remove" => false, "new" => true, "upsert" => true)) {
		// 不实现该方法
		throw new \Exception("mongodb 扩展不实现该方法");
	}

	/**
	 * 更新数据 - 使用$ince增加特定键的值
	 * @param array $data 要增加特定键的值
	 *          $data=array("fieldName"=>increaseValue);
	 * @return $this
	 */
	public function ince(array $data) {
		if (!key_exists('$inc', $this->update_data))
			$this->update_data['$inc'] = $data;
		else {
			$this->update_data['$inc'] = array_merge($this->update_data['$inc'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$set 修改键值
	 * @param array $data 要修改的键值数组
	 * @return $this
	 */
	public function set(array $data) {
		if (!key_exists('$set', $this->update_data))
			$this->update_data['$set'] = $data;
		else {
			$this->update_data['$set'] = array_merge($this->update_data['$set'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$unset 删除字段
	 * @param array $data 要删除的字段数组
	 * @return $this
	 */
	public function unSetfield(array $data) {
		if (!key_exists('$unset', $this->update_data))
			$this->update_data['$unset'] = $data;
		else {
			$this->update_data['$unset'] = array_merge($this->update_data['$unset'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$rename 重命名字段
	 * @param array $data 要重命名的字段数组
	 * @return $this
	 */
	public function rename(array $data) {
		if (!key_exists('$rename', $this->update_data))
			$this->update_data['$rename'] = $data;
		else {
			$this->update_data['$rename'] = array_merge($this->update_data['$rename'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$setOnInsert 更新/插入键值
	 * @param array $data 要更新/插入键值数组
	 * @return $this
	 */
	public function setOnInsert(array $data) {
		if (!key_exists('$setOnInsert', $this->update_data))
			$this->update_data['$setOnInsert'] = $data;
		else {
			$this->update_data['$setOnInsert'] = array_merge($this->update_data['$setOnInsert'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$push 向指定字段中添加值
	 * @param array $data 要更添加的键值数组
	 * @return $this
	 */
	public function push(array $data) {
		if (!key_exists('$push', $this->update_data))
			$this->update_data['$push'] = $data;
		else {
			$this->update_data['$push'] = array_merge($this->update_data['$push'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$push 和 $each 向指定字段中添加多个值
	 * @param $field 指定的字段
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function each($field, array $data) {
		$push_data = array($field => array('$each' => $data));
		return $this->push($push_data);
	}

	/**
	 * 更新数据 - 使用$addToSet 将数据添加到数组中
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function addToSet(array $data) {
		if (!key_exists('$addToSet', $this->update_data))
			$this->update_data['$addToSet'] = $data;
		else {
			$this->update_data['$addToSet'] = array_merge($this->update_data['$addToSet'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$addToSet 和 $each 向指定字段中添加多个值
	 * @param $field 更新匹配记录的对象
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function eachAddToSet($field, array $data) {
		$push_data = array($field => array('$each' => $data));
		return $this->addToSet($push_data);
	}

	/**
	 * 更新数据 - 使用$pop从指定字段中删除第一个或最后一个元素
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example
	 *  1、$client->pop(array("fieldname"=>1)) //删除最后一个元素;
	 *  2、$client->pop(array("fieldname"=> -1)) //删除最后一个元素;
	 * @return $this
	 */
	public function pop(array $data) {
		if (!key_exists('$pop', $this->update_data))
			$this->update_data['$pop'] = $data;
		else {
			$this->update_data['$pop'] = array_merge($this->update_data['$pop'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$pull 删除所指定值
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example $client->pull(array("e-mail"=>"****@qq.com"))
	 * @return $this
	 */
	public function pull(array $data) {
		if (!key_exists('$pull', $this->update_data))
			$this->update_data['$pull'] = $data;
		else {
			$this->update_data['$pull'] = array_merge($this->update_data['$pull'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$pull 删除所指定值
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example $client->pull(array("e-mail"=>array("****@qq.com",""****@gmail.com"")))
	 * @return $this
	 */
	public function pullAll(array $data) {
		if (!key_exists('$pullAll', $this->update_data))
			$this->update_data['$pullAll'] = $data;
		else {
			$this->update_data['$pullAll'] = array_merge($this->update_data['$pullAll'], $data);
		}
		return $this;
	}

	#endregion

	#region 删除数据

	/**
	 * 从集合中删除记录
	 * @param $collectionName 集合名称
	 * @param array $options [optional] 指定选项
	 * @return bool
	 */
	public function remove($collectionName, array $options = array("justOne" => false, "fsync" => false, "w" => 1, "j" => false)) {
		$this->_init();
		$bulk = new BulkWrite;
		$bulk->delete($this->where, ['limit' => $options["justOne"] ? 1 : 0]);
		$writeConcern = $this->_getWriteConcern($options);
		$result = $this->manager->executeBulkWrite("{$this->dbName}.{$collectionName}", $bulk, $writeConcern);
		if ($result->getDeletedCount()) {
			return true;
		}
		return false;
	}

	#endregion

	#region 查询数据

	/**
	 * 返回单个文档
	 * @param $collectionName 集合名称
	 * @return array
	 */
	public function findOne($collectionName) {
		$this->_init();
		// 查询数据
		$filter = $this->where;
		$this->where = array();
		$options = array("limit" => 1,
			"sort" => array("_id" => 1));
		if ($this->fields) {
			$options["projection"] = $this->fields;
			$this->fields = array();
		}
		$query = new Query($filter, $options);
		$readPreference = new ReadPreference(ReadPreference::RP_PRIMARY);
		$cursor = $this->manager->executeQuery("{$this->dbName}.{$collectionName}", $query, $readPreference);
		foreach ($cursor as $next) {
			$document = json_decode(json_encode($next), true);
			if (isset($document["_id"])) {
				$document["id"] = $next->_id->__toString();
				unset($document["_id"]);
			}
			return $document;
		}
		return array();
	}

	/**
	 * 返回所有文档
	 * @param $collectionName 集合名称
	 * @return array
	 */
	public function find($collectionName) {
		$this->_init();
		$filter = $this->where;
		$this->where = array();
		$options = array();
		if ($this->fields) {
			$options["projection"] = $this->fields;
			$this->fields = array();
		}
		// 设置排序
		if (isset($this->sort) && !empty($this->sort)) {
			$options["sort"] = $this->sort;
			$this->sort = array();
		}

		// 限制
		if (isset($this->limit) && intval($this->limit)) {
			$options["limit"] = $this->limit;
			$this->limit = 0;
		}

		// 忽略数据项
		if (isset($this->skip) && intval($this->skip)) {
			$options["skip"] = $this->skip;
			$this->skip = 0;
		}

		$query = new Query($filter, $options);
		$readPreference = new ReadPreference(ReadPreference::RP_PRIMARY);
		$cursor = $this->manager->executeQuery("{$this->dbName}.{$collectionName}", $query, $readPreference);
		$documents = array();
		foreach ($cursor as $next) {
			$document = json_decode(json_encode($next), true);
			$document["id"] = $next->_id->__toString();
			unset($document["_id"]);
			$documents[] = $document;
		}
		return $documents;
	}

	/**
	 * 返回集合中的文档数量
	 * @param $collectionName 集合名称
	 * @param bool $all 是否返回全部
	 * @return int
	 */
	public function count($collectionName, $all = FALSE) {
		$this->_init();
		$filter = $this->where;
		$this->where = array();
		$options = array();

		// 限制
		if (isset($this->limit) && intval($this->limit)) {
			$options["limit"] = $this->limit;
			$this->limit = 0;
		}

		// 忽略数据项
		if (isset($this->skip) && intval($this->skip)) {
			$options["skip"] = $this->skip;
			$this->skip = 0;
		}

		$query = new Query($filter, $options);
		$readPreference = new ReadPreference(ReadPreference::RP_PRIMARY);
		$cursor = $this->manager->executeQuery("{$this->dbName}.{$collectionName}", $query, $readPreference);
		return count($cursor->toArray());
	}



	#endregion

	#region 私有方法

	/**
	 * 初始化
	 */
	private function _init() {
		static $manager;
		if (!isset($manager) || !$manager) {
			$manager = new Manager("mongodb://{$this->config->host}:{$this->config->port}", $this->config->options->toArray());
		}
		$this->manager = $manager;
	}

	/**
	 * 获取 MongoDB\Driver\WriteConcern 对象
	 * @param $options
	 * @return \MongoDB\Driver\WriteConcern
	 */
	private function _getWriteConcern($options) {
		return new WriteConcern($options["w"], $options["j"], $options["fsync"]);
	}

	#endregion
}